/*
 *  Copyright 2005 Blandware (http://www.blandware.com)
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.blandware.atleap.persistence.hibernate.forum;

import com.blandware.atleap.common.Constants;
import com.blandware.atleap.common.parsers.exception.PlainTextExtractorException;
import com.blandware.atleap.common.parsers.html.HTMLPlainTextExtractor;
import com.blandware.atleap.common.util.ConvertUtil;
import com.blandware.atleap.common.util.PartialCollection;
import com.blandware.atleap.common.util.QueryInfo;
import com.blandware.atleap.model.core.*;
import com.blandware.atleap.model.forum.Forum;
import com.blandware.atleap.model.forum.Post;
import com.blandware.atleap.model.forum.Topic;
import com.blandware.atleap.persistence.forum.ForumDAO;
import com.blandware.atleap.persistence.forum.PostDAO;
import com.blandware.atleap.persistence.hibernate.core.PageDAOHibernate;

import java.io.ByteArrayInputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.*;

/**
 * <p>Hibernate implementation of ForumDAO.</p>
 * <p><a href="ForumDAOHibernate.java.html"><i>View source</i></a></p>
 *
 * @author Roman Puchkovskiy <a href="mailto:roman.puchkovskiy@blandware.com">
 *         &lt;roman.puchkovskiy@blandware.com&gt;</a>
 * @version $Revision: 1.13 $ $Date: 2008/06/23 13:24:54 $
 */
public class ForumDAOHibernate extends PageDAOHibernate implements ForumDAO {

    /**
     * DAO to work with posts
     */
    PostDAO postDAO;

    /**
     * Sets DAO to work with posts
     *
     * @param postDAO DAO to set
     */
    public void setPostDAO(PostDAO postDAO) {
        this.postDAO = postDAO;
    }

    // ~ CRUD Methods ================================================================

    /**
     * @see com.blandware.atleap.persistence.forum.ForumDAO#createForum(com.blandware.atleap.model.forum.Forum, com.blandware.atleap.model.forum.Forum, com.blandware.atleap.model.core.ContentLocale, java.util.Map)
     */
    public Long createForum(Forum forum, Forum parent, ContentLocale language,
                            Map linkedObjects) {
        if (parent != null) {
            parent.addChildForum(forum);
        }
        Long parentId = null;
        if (parent != null) {
            parentId = parent.getId();
        }
        forum.setUsageCounter(new Integer(0));
        forum.setLastUpdatedDatetime(new Date());
        forum.setActive(Boolean.TRUE);
        forum.setLanguage(language);
        forum.setPosition(getFirstFreePosition(parentId));
        Long forumId = (Long) getHibernateTemplate().save(forum);

		// title
		ContentField fTitle = new ContentField();
		fTitle.setIdentifier("title");
		fTitle.setType(ContentField.LINE_TYPE);
		fTitle.setInternal(Boolean.TRUE);
		forum.addContentField(fTitle);
		getHibernateTemplate().save(fTitle);

		// annotation
		ContentField fAnnotation = new ContentField();
		fAnnotation.setIdentifier("annotation");
		fAnnotation.setType(ContentField.HTML_TYPE);
		fAnnotation.setInternal(Boolean.TRUE);
		forum.addContentField(fAnnotation);
		getHibernateTemplate().save(fAnnotation);

        HTMLPlainTextExtractor extractor = new HTMLPlainTextExtractor();

        // values for title and annotation
        String valueTitle = (String) forum.getTitle().get(language.getIdentifier());
        String valueAnnotation = forum.getAnnotation();

        // title
		List contentLocales = executeFind("from ContentLocale l", true, "query.listContentLocales");
		for ( int i = 0; i < contentLocales.size(); i++ ) {
			ContentLocale contentLocale = (ContentLocale) contentLocales.get(i);
            ContentFieldValue fvTitle = new ContentFieldValue();
            fvTitle.setContentLocale(contentLocale);
            if ( valueTitle != null ) {
                fvTitle.setSimpleValue(valueTitle);
            } else {
                fvTitle.setSimpleValue("");
            }
            fvTitle.setLastUpdatedDatetime(new Date());
            fTitle.addContentFieldValue(fvTitle);
            getHibernateTemplate().save(fvTitle);
        }

        // annotation
        ContentFieldValue fvAnnotation = new ContentFieldValue();
        fvAnnotation.setContentLocale(language);

        if ( valueAnnotation != null ) {
            byte[] annotationBytes = ConvertUtil.convertToByteArray(valueAnnotation);
            fvAnnotation.setValue(annotationBytes);
            // get linked objects
            try {
                Set refs = new HashSet(extractor.extractAllRefs(new ByteArrayInputStream(annotationBytes), Constants.DEFAULT_ENCODING));
                List linkedPages = new ArrayList();
                List linkedResources = new ArrayList();
                for ( Iterator j = refs.iterator(); j.hasNext(); ) {
                    String ref = (String) j.next();
                    BaseObject linkedObject = (BaseObject) linkedObjects.get(ref);
                    if ( linkedObject != null ) {
                        if ( linkedObject instanceof ContentResource ) {
                            linkedResources.add(linkedObject);
                        } else if ( linkedObject instanceof Page ) {
                            linkedPages.add(linkedObject);
                        }
                    }
                }
                fvAnnotation.setLinkedResources(linkedResources);
                fvAnnotation.setLinkedPages(linkedPages);
            } catch ( PlainTextExtractorException e ) {
                // log exception
                if ( log.isErrorEnabled() ) {
                    StringWriter sw = new StringWriter();
                    e.printStackTrace(new PrintWriter(sw));
                    log.error(sw.toString());
                }
            }
        } else {
            fvAnnotation.setValue(new byte[0]);
        }

        fvAnnotation.setLastUpdatedDatetime(new Date());
        fAnnotation.addContentFieldValue(fvAnnotation);
        getHibernateTemplate().save(fvAnnotation);

        return forumId;
    }

    /**
     * @see com.blandware.atleap.persistence.forum.ForumDAO#retrieveForum(Long)
     */
    public Forum retrieveForum(Long forumId) {
		String hql = new StringBuffer("select forum, title.simpleValue, annotation.value ")
		        .append("from Forum as forum left outer join ")
		        .append("forum.contentFields titleField left outer join titleField.contentFieldValues as title left outer join ")
                .append("forum.contentFields annotationField left outer join annotationField.contentFieldValues as annotation ")
		        .append("where ")
		        .append("titleField.identifier = 'title' and annotationField.identifier = 'annotation' and forum.id = ?")
		        .toString();

        // Get a list of triads (forum, title value, annotation value)
		List list = executeFind(hql, new Object[]{forumId});
        // Assign those values to corresponding forums -- get list of items with
        // title and annotation correctly assigned
        Map title;
        for ( ListIterator i = list.listIterator(); i.hasNext(); ) {
            Object[] objects = (Object[]) i.next();
            Forum forum = (Forum) objects[0];
            title = new HashMap();
            title.put(forum.getLanguage().getIdentifier(), objects[1]);
            forum.setTitle(title);
            forum.setAnnotation(ConvertUtil.convertToString((byte[]) objects[2]));
            forum.setNumberOfTopics(getNumberOfTopics(forumId));
            forum.setNumberOfReplies(getNumberOfReplies(forumId));
            forum.setUsersSubscribed(getUsersSubscribedMap(forum.getId()));
            forum.setLastPost(getLastPost(forum.getId()));
            i.set(forum);
        }
		if (list.size() <= 0) {
			return null;
		} else {
			return (Forum) list.get(0);
		}
    }

    /**
     * @see com.blandware.atleap.persistence.forum.ForumDAO#updateForum(com.blandware.atleap.model.forum.Forum, com.blandware.atleap.model.forum.Forum, java.util.Map)
     */
    public void updateForum(Forum forum, Forum parent, Map linkedObjects) {
        if (parent != null) {
            Forum oldForum = parent.updateChildForum(forum);
            removeFromCache(oldForum);
        }
        getHibernateTemplate().update(forum);

		HTMLPlainTextExtractor extractor = new HTMLPlainTextExtractor();

        // Update internal fields (title and annotation)
		for ( int i = 0; i < forum.getContentFields().size(); i++ ) {
			ContentField contentField = (ContentField) forum.getContentFields().get(i);
			String value;
			boolean blob = true;
            if ( "title".equalsIgnoreCase(contentField.getIdentifier()) ) {
				value = forum.getSingleTitle();
                blob = false;
			} else if ( "annotation".equalsIgnoreCase(contentField.getIdentifier()) ) {
				value = forum.getAnnotation();
			} else {
				continue;
			}

            for (Iterator cfvIterator = contentField.getContentFieldValues().iterator(); cfvIterator.hasNext(); ) {
                ContentFieldValue contentFieldValue = (ContentFieldValue) cfvIterator.next();
                if ( value != null ) {
                    if ( !blob ) {
                        contentFieldValue.setSimpleValue(value);
                    } else {
                        byte[] valueBytes = ConvertUtil.convertToByteArray(value);
                        contentFieldValue.setValue(valueBytes);
                        if ( linkedObjects != null ) {
                            // get linked objects
                            try {
                                Set refs = new HashSet(extractor.extractAllRefs(new ByteArrayInputStream(valueBytes), Constants.DEFAULT_ENCODING));
                                List linkedPages = new ArrayList();
                                List linkedResources = new ArrayList();
                                for ( Iterator k = refs.iterator(); k.hasNext(); ) {
                                    String ref = (String) k.next();
                                    BaseObject linkedObject = (BaseObject) linkedObjects.get(ref);
                                    if ( linkedObject != null ) {
                                        if ( linkedObject instanceof ContentResource ) {
                                            linkedResources.add(linkedObject);
                                        } else if ( linkedObject instanceof Page ) {
                                            linkedPages.add(linkedObject);
                                        }
                                    }
                                }
                                contentFieldValue.setLinkedResources(linkedResources);
                                contentFieldValue.setLinkedPages(linkedPages);
                            } catch ( PlainTextExtractorException e ) {
                                // log exception
                                if ( log.isErrorEnabled() ) {
                                    StringWriter sw = new StringWriter();
                                    e.printStackTrace(new PrintWriter(sw));
                                    log.error(sw.toString());
                                }
                            }
                        }
                    }
                } else {
                    if ( !blob ) {
                        contentFieldValue.setSimpleValue("");
                    } else {
                        contentFieldValue.setValue(new byte[0]);
                    }
                }
                contentFieldValue.setLastUpdatedDatetime(new Date());
                contentFieldValue.getContentField().updateContentFieldValue(contentFieldValue);
                getHibernateTemplate().update(contentFieldValue);
            }
        }
    }

    /**
     * @see com.blandware.atleap.persistence.forum.ForumDAO#deleteForum(com.blandware.atleap.model.forum.Forum)
     */
    public void deleteForum(Forum forum) {
        removeAssociationsRecursively(forum);

        getHibernateTemplate().delete(forum);
        Forum parent = forum.getParent();
        if (parent != null) {
            parent.removeChildForum(forum);
        }
    }

    // ~ Additional methods ================================================================

    /**
     * @see com.blandware.atleap.persistence.forum.ForumDAO#listForums(com.blandware.atleap.common.util.QueryInfo)
     */
    public PartialCollection listForums(QueryInfo queryInfo) {
		String whereClause = "";
		String orderByClause = "";
		if ( queryInfo != null ) {
			whereClause = queryInfo.getWhereClause();
			orderByClause = queryInfo.getOrderByClause();
		}

		if ( whereClause == null || whereClause.length() == 0 ) {
			whereClause = "";
		}
		if ( orderByClause != null && orderByClause.length() != 0 ) {
			orderByClause = " order by " + orderByClause;
		} else {
			orderByClause = " order by f.position";
		}

        Long parentId = null;
        boolean ignoreParent = false;
        if ( queryInfo != null ) {
            if ( queryInfo.getQueryParameters() != null ) {
                parentId = (Long) queryInfo.getQueryParameters().get("parentId");
                Boolean ignore = (Boolean) queryInfo.getQueryParameters().get("ignoreParent");
                if (ignore != null) {
                    ignoreParent = ignore.booleanValue();
                }
            }
        }

		List list;
		Integer total = null;

		String hqlPart;
		ArrayList args = new ArrayList();
        hqlPart = new StringBuffer("from Forum as f ")
                .append("left outer join f.contentFields as titleField ")
                .append("left outer join titleField.contentFieldValues as title ")
                .append("left outer join f.contentFields as annotationField ")
                .append("left outer join annotationField.contentFieldValues as annotation ")
		        .append("left outer join f.roles as role ")
                .append("where ")
                .append("titleField.identifier = 'title' ")
                .append("and annotationField.identifier = 'annotation' ")
                .append("and title.contentLocale = f.language ")
                .append("and annotation.contentLocale = f.language ")
                .toString();
        if (!ignoreParent) {
            if ( parentId != null ) {
                hqlPart += "and f.parent.id = ? ";
                args.add(parentId);
            } else {
                hqlPart += "and f.parent is null ";
            }
        }
        if (whereClause.length() > 0) {
            hqlPart = hqlPart + "and " + whereClause;
        }

		if ( queryInfo != null && queryInfo.getLimit() != null && queryInfo.getOffset() != null ) {
			// query count
			String hqlForTotal = "select count(distinct f.id) " + hqlPart;
			total = findUniqueIntegerResult(hqlForTotal, args.toArray());
			if ( total == null ) {
				total = new Integer(0);
			}
		}

        // If we don't have any info about the total number of results yet or
        // we know that there's something that will be found, then fetch data
		if ( total == null || total.intValue() > 0 ) {
			String hql;
            hql = "select f, title.simpleValue, annotation.value " + hqlPart + orderByClause;
            list = executeFind(hql, queryInfo, args.toArray());
            if (list == null) {
                list = new ArrayList();
            }
            for ( ListIterator i = list.listIterator(); i.hasNext(); ) {
                Object[] objects = (Object[]) i.next();
                Forum forum = (Forum) objects[0];
                Map title = new HashMap();
                title.put(forum.getLanguage().getIdentifier(), objects[1]);
                forum.setTitle(title);
                forum.setAnnotation(ConvertUtil.convertToString((byte[]) objects[2]));
                i.set(forum);
			}
            List forums = new ArrayList();
            Set ids = new HashSet();
            for ( ListIterator i = list.listIterator(); i.hasNext(); ) {
                Forum forum = (Forum) i.next();
                if (!ids.contains(forum.getId())) {
                    forum.setNumberOfTopics(getNumberOfTopics(forum.getId()));
                    forum.setNumberOfReplies(getNumberOfReplies(forum.getId()));
                    forum.setUsersSubscribed(getUsersSubscribedMap(forum.getId()));
                    forum.setLastPost(getLastPost(forum.getId()));
                    forums.add(forum);
                    ids.add(forum.getId());
                }
            }
            list = forums;
            if ( total == null ) {
                total = new Integer(list.size());
            }
		} else {
			list = new ArrayList();
		}

		return new PartialCollection(list, total);

    }

    /**
     * @see com.blandware.atleap.persistence.forum.ForumDAO#hasDuplicates(com.blandware.atleap.model.forum.Forum, Long)
     */
    public boolean hasDuplicates(Forum forum, Long parentId) {

        ArrayList args = new ArrayList();
        args.add(forum.getSingleTitle());

        String hql = new StringBuffer("select count(distinct f.id) from Forum f ")
                .append("left outer join f.contentFields as titleField ")
                .append("left outer join titleField.contentFieldValues as title ")
                .append("where titleField.identifier = 'title' ")
                .append("and title.simpleValue = ? ")
                .toString();
        if (parentId == null) {
            hql += "and f.parent is null ";
        } else {
            hql += "and f.parent.id = ? ";
            args.add(parentId);
        }
        if ( forum.getId() != null ) {
            hql += " and f.id != ?";
            args.add(forum.getId());
        }

        int count = (findUniqueIntegerResult(hql, args.toArray())).intValue();

        return count > 0;
    }

    /**
     * @see com.blandware.atleap.persistence.forum.ForumDAO#moveForum(com.blandware.atleap.model.forum.Forum, boolean)
     */
    public void moveForum(Forum forum, boolean up) {
        boolean isRoot = forum.getParent() == null;
        Forum neighbourForum;
        String hql;
        String parentHql;
        List args;

        if (isRoot) {
            parentHql = "and f.parent is null";
        } else {
            parentHql = "and f.parent.id = ?";
        }
        if (up) {
            hql = "select max(f.position) from Forum f where f.position < ? ";
        } else {
            hql = "select min(f.position) from Forum f where f.position > ? ";
        }
        String neighbourPositionHql = hql + parentHql;
        args = new ArrayList();
        args.add(forum.getPosition());
        if (!isRoot) {
            args.add(forum.getParent().getId());
        }

        Integer neighbourPosition = findUniqueIntegerResult(neighbourPositionHql, args.toArray());
        if (neighbourPosition != null) {
            String neighbourHql = "select f from Forum f where f.position = ? " + parentHql;
            args = new ArrayList();
            args.add(neighbourPosition);
            if (!isRoot) {
                args.add(forum.getParent().getId());
            }
            neighbourForum = (Forum) findUniqueResult(neighbourHql, args.toArray());
            if (neighbourForum != null) {
                Integer tempPosition = forum.getPosition();
                forum.setPosition(neighbourForum.getPosition());
                neighbourForum.setPosition(tempPosition);

                getHibernateTemplate().update(forum);
                getHibernateTemplate().update(neighbourForum);
            }
        }
    }

    /**
     * @see com.blandware.atleap.persistence.forum.ForumDAO#getFirstLastOccupiedPositions(Long)
     */
    public Integer[] getFirstLastOccupiedPositions(Long parentId) {
        boolean isRoot = parentId == null;
        ArrayList args = new ArrayList();
        String hql = "select min(f.position), max(f.position) from Forum f where ";
        if (isRoot) {
            hql += "f.parent is null";
        } else {
            hql += "f.parent.id = ?";
            args.add(parentId);
        }
        Object[] objs = (Object[]) findUniqueResult(hql, args.toArray());
        Integer[] result = new Integer[2];
        result[0] = (Integer) objs[0];
        result[1] = (Integer) objs[1];
        return result;
    }

    /**
     * Returns the least position available among forums with a parent with
     * given ID. If parentId is <code>null</code>, root forums are considered.
     *
     * @param parentId ID of forum which children will be considered
     * @return the least free position
     */
    protected Integer getFirstFreePosition(Long parentId) {
        Integer result = getFirstLastOccupiedPositions(parentId)[1];
        if (result == null) {
            result = new Integer(0);
        } else {
            result = new Integer(result.intValue() + 1);
        }
        return result;
    }

    /**
     * Returns number of topics in a forum.
     *
     * @param forumId ID of forum
     * @return number of topics
     */
    protected Integer getNumberOfTopics(Long forumId) {
        String hql = new StringBuffer("select count(topic) from Topic as topic ")
                .append("where topic.forum.id = ? ")
                .toString();
        return findUniqueIntegerResult(hql, new Object[]{forumId});
    }

    /**
     * Returns number of replies in all topics of a forum.
     *
     * @param forumId ID of forum
     * @return number of replies
     */
    protected Integer getNumberOfReplies(Long forumId) {
        String hql = new StringBuffer("select count(post) from Topic as topic ")
                .append("left outer join topic.posts as post ")
                .append("where topic.forum.id = ? ")
                .append("and post.first != 'T' ")
                .toString();
        return findUniqueIntegerResult(hql, new Object[]{forumId});
    }

    /**
     * Returns a map which maps username to <code>Boolean.TRUE</code> if and
     * only if that user is subscribed to this forum.
     *
     * @param forumId forum ID
     * @return map
     */
    protected Map getUsersSubscribedMap(Long forumId) {
        String hql = new StringBuffer("select fs.user.name from ForumSubscription as fs ")
                .append("where fs.forum.id = ? ")
                .append("and fs.user is not null ")
                .toString();
        List list = executeFind(hql, new Object[]{forumId});
        Map result = new HashMap();
        for (ListIterator i = list.listIterator(); i.hasNext(); ) {
            result.put(i.next(), Boolean.TRUE);
        }
        return result;
    }

    /**
     * Returns last post in a forum with a given ID. If there're no posts in
     * this forum, <code>null</code> is returned.
     *
     * @param forumId ID of forum
     * @return last post in a forum
     */
    protected Post getLastPost(Long forumId) {
        String hql = new StringBuffer("select post.id from Post as post ")
                .append("where post.topic.forum.id = ? ")
                .append("order by post.date desc ")
                .toString();
        QueryInfo queryInfo = new QueryInfo();
        queryInfo.setOffset(new Integer(0));
        queryInfo.setLimit(new Integer(1));
        Long postId = null;
        List ids = executeFind(hql, queryInfo, new Object[]{forumId});
        if (ids != null && ids.size() > 0) {
            postId = (Long) ids.get(0);
        }
        Post post = null;
        if (postId != null) {
            post = postDAO.retrievePost(postId);
        }
        return post;
    }

    /**
     * Removes associations for a given forum and all its subforums recursively.
     *
     * @param forum forum from which to start
     */
    protected void removeAssociationsRecursively(Forum forum) {
        // remove associations from forum itself
        removeAssociations(forum);
        // remove associations from posts in forum topics
        for (Iterator i = forum.getTopics().iterator(); i.hasNext(); ) {
            Topic topic = (Topic) i.next();
            for (Iterator j = topic.getPosts().iterator(); j.hasNext(); ) {
                Post post = (Post) j.next();
                postDAO.removeAssociations(post);
            }
        }
        // remove associations from child forums recursively
        for (Iterator i = forum.getChildForums().iterator(); i.hasNext(); ) {
            Forum child = (Forum) i.next();
            removeAssociationsRecursively(child);
        }
    }
}
